using System;
using System.Collections.Generic;
using System.Text;
using PDSClient.Utils.Shared;
using PDSClient.Communication;
using PDSClient.Simple.Impl;
using PDSClient.Utils;
using System.IO;

namespace PDSClient.Simple
{
    public class SimpleClient : IServerSession
    {

        private  readonly CompactId SERVER_ID = new CompactId(new byte[] { (byte)0 });

        private readonly IClientConnectionListener connListener;

        private IDictionary<CompactId, SimpleClientChannel> channels;

        private ISimpleClientListener clientListener;

        private IClientConnection clientConnection = null;

        private bool connectionStateChanging = false;

        private bool expectingDisconnect = false;

        private SessionId sessionId;

        private long sequenceNumber = 0; // was AtomicLong

        private byte[] reconnectKey;

        public SimpleClient(ISimpleClientListener listener)
        {
            connListener = new SimpleClientConnectionListener(this);
            if (listener == null)
                throw new NullReferenceException("the simpleclientListener argument must not be null");

            this.clientListener = listener;
        }

        public void Login(IDictionary<string, string> props)
        {
            lock (this) {
                if (connectionStateChanging || clientConnection != null) 
                {
                    Exception e = new Exception("IllegalState, Session already connected or connecting");
                    //logging...
                    throw e;
                }
                connectionStateChanging = true;
            }
            ClientConnector connector = ClientConnector.Create(props);
            connector.Connect(connListener);
        }

        public SessionId SessionId
        {
            get { CheckConnected(); return sessionId; }
        }

        public void Send(byte[] message)
        {
            CheckConnected();
            MessageBuffer msg = new MessageBuffer(3 + 8 + 2 + message.Length);
            msg.PutByte(SimpleSgsProtocol.VERSION).
                PutByte(SimpleSgsProtocol.APPLICATION_SERVICE).
                PutByte(SimpleSgsProtocol.SESSION_MESSAGE).
                PutLong(sequenceNumber++).
                PutByteArray(message);
            SendRaw(msg.GetBuffer());
        }

        private void SendRaw(byte[] data)
        {
            clientConnection.SendMessage(data);
        }

        private void CheckConnected() 
        {
            if (!Connected)
            {
                Exception e = new Exception("IllegalState, Client not connected");
                //logging logger.logThrow(Level.FINE, re, re.getMessage());
                throw e;
            }
        }
        private void CheckLoggedIn()
        {
            if (SessionId == null) 
            {
                Exception e = new Exception("IllegalState, Client not logged in");
                //logging logger.logThrow(Level.FINE, re, re.getMessage());
                throw e;
            }
        }

        public bool Connected
        {
            get { return clientConnection != null; }
        }

        public void Logout(bool force)
        {
            lock (this) {
                if (connectionStateChanging || clientConnection == null)
                {
                    Exception e = new Exception("IllegalState, Client not connected");
                    //logging
                    throw e;
                }
                connectionStateChanging = true;
            }
            if (force)
            {
                try
                {
                    clientConnection.Disconnect();
                }
                catch (Exception e) //IOException
                {
                    //logging logger.logThrow(Level.FINE, e, "During forced logout:");
                }
            }
            else 
            {
                try
                {
                    MessageBuffer msg = new MessageBuffer(3);
                    msg.PutByte(SimpleSgsProtocol.VERSION).
                        PutByte(SimpleSgsProtocol.APPLICATION_SERVICE).
                        PutByte(SimpleSgsProtocol.LOGOUT_REQUEST);
                    SendRaw(msg.GetBuffer());
                }
                catch (Exception e) // IOException
                {
                    //logging logger.logThrow(Level.FINE, e, "During graceful logout:");
                    try
                    {
                        clientConnection.Disconnect();
                    }
                    catch (Exception e2) // IOException
                    {
                        //logging logger.logThrow(Level.FINE, e2, "During forced logout:");
                    }
                }
            }
        }

        private class SimpleClientConnectionListener : IClientConnectionListener
        {
            private SimpleClient outer;

            public SimpleClientConnectionListener(SimpleClient outer)
            {
                this.outer = outer;            
            }

            public void Connected(IClientConnection connection)
            {
                //logging logger.log(Level.FINER, "Connected");
                lock (outer)
                {
                    outer.connectionStateChanging = false;
                    outer.clientConnection = connection;
                }

                PasswordAuthentication authentication = outer.clientListener.PasswordAuthentication;

                if (authentication == null)
                {
                    outer.Logout(true);
                    throw new NullReferenceException("the returned PasswordAuthentication must not be null");
                }

                string user = authentication.UserName;
                string pass = new string(authentication.Password);
                MessageBuffer msg = 
                    new MessageBuffer(3 + 
                    MessageBuffer.GetSize(user) + 
                    MessageBuffer.GetSize(pass));
                msg.PutByte(SimpleSgsProtocol.VERSION).
                    PutByte(SimpleSgsProtocol.APPLICATION_SERVICE).
                    PutByte(SimpleSgsProtocol.LOGIN_REQUEST).
                    PutString(user).
                    PutString(pass);
                try
                {
                    outer.SendRaw(msg.GetBuffer());
                }
                catch (Exception e)
                {
                    //logging logger.logThrow(Level.FINE, e, "During login request:");
                    outer.Logout(true);
                }
            }
            public void Disconnected(bool graceful, byte[] message) 
            {
                lock (outer)
                {
                    if (outer.clientConnection == null && (!outer.connectionStateChanging))
                    {
                        return;
                    }
                    outer.clientConnection = null;
                    outer.connectionStateChanging = false;
                }
                outer.sessionId = null;
                string reason = null;
                if (message != null)
                {
                    MessageBuffer msg = new MessageBuffer(message);
                    reason = msg.GetString();
                }

                foreach(SimpleClientChannel channel in outer.channels.Values)
                {
                    try
                    {
                        channel.Left();
                    }
                    catch (Exception e)
                    { 
                        //logging                     logger.logThrow(Level.FINE, e,
                        //"During leftChannel ({0}) on disconnect:",
                        //channel.getName());

                        //ignore the exception
                    }
                }
                outer.channels.Clear();

                // FIXME (from the java API) ignore graceful from connection for now (not implemented),
                // instead look at the boolean we set when expecting disconnect
                outer.clientListener.Disconnected(outer.expectingDisconnect, reason);
                outer.expectingDisconnect = false;
            }

            public void ReceivedMessage(byte[] message) 
            {
                try
                {
                    MessageBuffer msg = new MessageBuffer(message);
                    byte version = msg.GetByte();
                    if (version != SimpleSgsProtocol.VERSION)
                        throw new IOException(String.Format("bad version {0}, wanted: {1}", Convert.ToString(version),Convert.ToString(SimpleSgsProtocol.VERSION)));

                    byte service = msg.GetByte();

                    /*
                    if (logger.isLoggable(Level.FINER))
                    {
                        String logMessage = String.format(
                            "Message length:%d service:0x%02X",
                            message.length,
                            service);
                        logger.log(Level.FINER, logMessage);
                    }
                    */

                    switch (service)
                    {
                        case SimpleSgsProtocol.APPLICATION_SERVICE:
                            HandleApplicationMessage(msg);
                            break;

                        case SimpleSgsProtocol.CHANNEL_SERVICE:
                            HandleChannelMessage(msg);
                            break;
                        default:
                            throw new IOException(String.Format("unknown service {0}", Convert.ToString(service)));
                    }
                }
                catch (IOException e)
                {
                    //logging logger.logThrow(Level.FINER, e, e.getMessage());
                    if (outer.Connected)
                    {
                        try
                        {
                            outer.clientConnection.Disconnect();
                        }
                        catch (Exception e2)
                        { 
                            //logging logger.logThrow(Level.FINEST, e2,
                            // "Disconnect failed after {0}", e.getMessage());
                            
                            //ignore
                        }
                    }
                }
            }

            private void HandleApplicationMessage(MessageBuffer msg)
            {
                byte command = msg.GetByte();
                switch (command)
                {
                    case SimpleSgsProtocol.LOGIN_SUCCESS:
                        //logging logger.log(Level.FINER, "Logged in");
                        byte[] idBytes = CompactId.GetCompactId(msg).Id;
                        outer.sessionId = SessionId.FromBytes(idBytes);
                        idBytes = CompactId.GetCompactId(msg).Id;
                        outer.reconnectKey = idBytes;
                        outer.clientListener.LoggedIn();
                        break;
                    case SimpleSgsProtocol.LOGIN_FAILURE:
                        //logging logger.log(Level.FINER, "Login failed");
                        outer.clientListener.LoginFailed(msg.GetString());
                        break;
                    case SimpleSgsProtocol.SESSION_MESSAGE:
                        //logging logger.log(Level.FINEST, "Direct receive");
                        outer.CheckLoggedIn();
                        msg.GetLong(); //FIXME (original API) sequence number
                        outer.clientListener.ReceivedMessage(msg.GetByteArray());
                        break;
                    case SimpleSgsProtocol.RECONNECT_SUCCESS:
                        // logger.log(Level.FINER, "Reconnected");
                        outer.clientListener.Reconnected();
                        break;
                    case SimpleSgsProtocol.RECONNECT_FAILURE:
                        try
                        {
                            //logging logger.log(Level.FINER, "Reconnect failed");
                            outer.clientConnection.Disconnect();
                        }
                        catch (Exception e) //IOException
                        {
                            /*
                            if (logger.isLoggable(Level.FINE)) {
                                logger.logThrow(Level.FINE, e,
                                "Disconnecting a failed reconnect");
                            }
                            */
                            //ignore
                        }
                        break;
                    case SimpleSgsProtocol.LOGOUT_SUCCESS:
                        //logging logger.log(Level.FINER, "Logged out gracefully");
                        outer.expectingDisconnect = true;
                        break;

                    default:
                        throw new IOException(String.Format("unknown session opcode: {0}", command));
                }
            }
            private void HandleChannelMessage(MessageBuffer msg)
            {
                byte command = msg.GetByte();
                switch (command)
                {
                    case SimpleSgsProtocol.CHANNEL_JOIN: {
                        //logging logger.log(Level.FINER, "Channel join");
                        outer.CheckLoggedIn();
                        string channelName = msg.GetString();
                        CompactId channelId = CompactId.GetCompactId(msg);
                        SimpleClientChannel channel = new SimpleClientChannel(channelName, channelId, outer);

                        /* was :
                        if (channels.putIfAbsent(channelId, channel) == null) {
                            channel.joined();
                        */
                        if (!outer.channels.Contains(new KeyValuePair<CompactId, SimpleClientChannel>(channelId, channel)))
                        {
                            outer.channels.Add(channelId, channel);
                            channel.Joined();
                        }
                        else
                        {
                            /*
                            logger.log(Level.WARNING,
                                "Cannot join channel {0}: already a member",
                                channelName); 
                             */
                        }
                        break;
                    }
                    case SimpleSgsProtocol.CHANNEL_LEAVE: {
                        //logging logger.log(Level.FINER, "Channel leave");
                        outer.CheckLoggedIn();
                        CompactId channelId = CompactId.GetCompactId(msg);
                        SimpleClientChannel channel = outer.channels[channelId];
                        outer.channels.Remove(channelId);
                        if (channel != null)
                            channel.Left();
                        else
                        {
                            /*
                            logger.log(Level.WARNING,
                            "Cannot leave channel {0}: not a member",
                            channelId);
                             */
                        }
                        break;
                    }
                    case SimpleSgsProtocol.CHANNEL_MESSAGE: {
                        //logging logger.log(Level.FINEST, "Channel recv");
                        outer.CheckLoggedIn();
                        CompactId channelId = CompactId.GetCompactId(msg);
                        SimpleClientChannel channel = outer.channels[channelId];
                        if (channel == null)
                        {
                            /*
                            logger.log(Level.WARNING,
                                "Ignore message on channel {0}: not a member",
                                channelId);
                            */
                            return;                        
                        }
                        msg.GetLong(); // FIXME(java API) sequence number

                        CompactId compactSessionId = CompactId.GetCompactId(msg);
                        SessionId sid =
                            compactSessionId.Equals(outer.SERVER_ID) ? null : SessionId.FromBytes(compactSessionId.Id);
                        channel.ReceivedMessage(sid, msg.GetByteArray());
                        break;
                    }
                    default:
                        throw new IOException(String.Format("Unknown channel opcode: {0}", command));
                }
            }
            public IServerSessionListener SessionStarted(byte[] message)
            {
                Exception e = new Exception("Not supported by SimpleClient");
                //logger.logThrow(Level.WARNING, re, re.getMessage());
                throw e;
            }

            public void Reconnecting(byte[] message)
            {
                Exception e = new Exception("Not supported by SimpleClient");
                //logger.logThrow(Level.WARNING, re, re.getMessage());
                throw e;
            }

            public void Reconnected(byte[] message)
            {
                Exception e = new Exception("Not supported by SimpleClient");
                //logger.logThrow(Level.WARNING, re, re.getMessage());
                throw e;
            }
        }


        internal class SimpleClientChannel : IClientChannel
        {
            private string channelName; // was final

            private CompactId channelId; // was final

            private IClientChannelListener listener = null; // was volatile

            private bool isJoined = false; // was AtomicBoolean

            SimpleClient outer; // here to mimic java's inner class behavior.

            private SimpleClientChannel(SimpleClient outer)
            {
                this.outer = outer;
            }
            public SimpleClientChannel(string name, CompactId id, SimpleClient outer) : this(outer)
            {
                this.channelName = name;
                this.channelId = id;
            }
            public string Name
            {
                get { return channelName; }
            }

            public void Send(byte[] message)
            {
                SendInternal(null, message);
            }

            public void Send(SessionId recipient, byte[] message)
            {
                List<SessionId> rcp = new List<SessionId>();
                rcp.Add(recipient);
                SendInternal(rcp, message);
            }

            public void Send(ICollection<SessionId> recipients, byte[] message)
            {
                SendInternal(recipients, message);
            }
            public void Joined()
            {
                if (isJoined)
                    throw new Exception("IllegalState, Already joined to channel " + channelName);

                isJoined = true;

                try
                { 
                    listener = outer.clientListener.JoinedChannel(this);

                    if (listener == null)
                        throw new NullReferenceException("the returned IClientChannelListener must not be null");
                }
                catch (Exception e)
                {
                    isJoined = false;
                    throw e;
                }
            }

            public void Left() 
            {
                if (!isJoined)
                    throw new Exception("IllegalState, Cannot leave unjoined channel " + channelName);

                IClientChannelListener l = this.listener;
                this.listener = null;

                l.LeftChannel(this);
            }

            public void ReceivedMessage(SessionId sid, byte[] message)
            {
                if (!isJoined)
                    throw new Exception("IllegalState, Cannot receive on unjoined channel " + channelName);

                listener.ReceivedMessage(this, sid, message);
            }

            public void SendInternal(ICollection<SessionId> recipients, byte[] message)
            {
                if (!isJoined)
                    throw new Exception("IllegalState, Cannot send on unjoined channel " + channelName);

                int totalSessionLength = 0;
                if (recipients != null)
                {
                    foreach (SessionId recipientId in recipients)
                    { 
                        totalSessionLength += ((SimpleSessionId) recipientId)
                            .CompactId.ExternalFormByteCount;
                    }
                }

                MessageBuffer msg =
                    new MessageBuffer(3 +
                    channelId.ExternalFormByteCount +
                    8 +
                    2 + totalSessionLength +
                    2 + message.Length);
                msg.PutByte(SimpleSgsProtocol.VERSION).
                    PutByte(SimpleSgsProtocol.CHANNEL_SERVICE).
                    PutByte(SimpleSgsProtocol.CHANNEL_SEND_REQUEST).
                    PutBytes(channelId.ExternalForm).
                    PutLong(outer.sequenceNumber++);
                if (recipients == null)
                    msg.PutShort(0);
                else
                {
                    msg.PutShort(recipients.Count);
                    foreach (SessionId recipientId in recipients)
                    {
                        msg.PutBytes(((SimpleSessionId)recipientId).
                            CompactId.ExternalForm);
                    }
                }
                msg.PutByteArray(message);
                outer.SendRaw(msg.GetBuffer());
            }
        }
    }
}
